// Copyright (C) Kumo inc. and its affiliates.
// Author: Jeff.li lijippy@163.com
// All rights reserved.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
#pragma once

#include <mutex>
#include <string>
#include <unordered_map>
#include <melon/container/f14_map.h>
#include <pollux/common/base/exceptions.h>
#include <pollux/common/base/macros.h>

namespace kumo::pollux::common::testutil {
    /// The test facility used to inject callback hook in production execution point
    /// to get, change and verify various internal execution state for unit test.
    /// The hook injected in production code path will be opt out in release build
    /// and only be executed in debug build. Please follow the example in
    /// TestValueTest.cpp for use.
    class TestValue {
    public:
        /// Enable the test value injection.
        static void enable();

        /// Disable the test value injection.
        static void disable();

        /// Check if the test value injection is enabled or not.
        static bool enabled();

        /// Invoked by the test code to register a callback hook at the specified
        /// execution point. 'injectionPoint' is a string to identify the execution
        /// point which could be formed by concatenating namespace, class name, method
        /// name plus optional actual action within a method. 'injectionCb' is the
        /// injected callback hook.
        template<typename T>
        static void set(
            const std::string &injectionPoint,
            std::function<void(T *)> injectionCb);

        /// Invoked by the test code to unregister a callback hook at the specified
        /// execution point.
        static void clear(const std::string &injectionPoint);

        /// Invoked by the production code to try to invoke the test callback hook
        /// with 'testData' if there is one registered at the specified execution
        /// point. 'testData' capture the mutable production execution state.
        static void adjust(const std::string &injectionPoint, void *testData);

    private:
        using Callback = std::function<void(void *)>;

        static std::mutex mutex_;
        static bool enabled_;
        static melon::F14FastMap<std::string, Callback> injectionMap_;
    };

    class ScopedTestValue {
    public:
        template<typename T>
        ScopedTestValue(const std::string &point, std::function<void(T *)> callback)
            : point_(point) {
            POLLUX_CHECK_NOT_NULL(callback);
            POLLUX_CHECK(!point_.empty());
            TestValue::set<T>(point_, std::move(callback));
        }

        ~ScopedTestValue() {
            TestValue::clear(point_);
        }

    private:
        const std::string point_;
    };

#ifndef NDEBUG
    template<typename T>
    void TestValue::set(
        const std::string &injectionPoint,
        std::function<void(T *)> injectionCb) {
        std::lock_guard<std::mutex> l(mutex_);
        if (!enabled_) {
            return;
        }
        injectionMap_[injectionPoint] = [injectionCb](void *testData) {
            T *typedData = static_cast<T *>(testData);
            injectionCb(typedData);
        };
    }
#else
template <typename T>
void TestValue::set(
    const std::string& injectionPoint,
    std::function<void(T*)> injectionCb) {}
#endif

#ifdef NDEBUG
// Keep the definition in header so that it can be inlined (elided).
inline void TestValue::adjust(
    const std::string& injectionPoint,
    void* testData) {}
#endif

#define SCOPED_TESTVALUE_SET(point, ...)                              \
  ::kumo::pollux::common::testutil::ScopedTestValue POLLUX_VARNAME( \
      _scopedTestValue)(point, ##__VA_ARGS__)
} // namespace kumo::pollux::common::testutil
